-- tdc.lua:  Traitor debugging utility
--
-- Implements the "/td" chat command.
-- Type "/td help" for info on how to use it.

local TAB = '   '

local function _keyw(msg)
    return minetest.colorize('#a0a0ff', msg)
end

local function _info(msg)
    return minetest.colorize('#ffff80', msg)
end

local function _stdout(msg)
    return minetest.colorize('#80ddee', msg)
end

local function _lsitem(item)
    return TAB .. '- ' .. item
end

function list_actions()
    local hdr = 'Available commands: '
    local msgtab = {}
    for cmd in pairs(tdc.actions) do
        table.insert(msgtab, _keyw(cmd))
    end
    return hdr .. table.concat(msgtab, '|')
end

-- return corpse count / info messages for a list <ctab> of node positions
local function list_corpses(ctab)
    local node, meta
    local count = 0
    local corpses = {}
    for _, pos in ipairs(ctab) do
        node = minetest.get_node(pos)
        meta = minetest.get_meta(pos)
        if node and node.name and node.name ~= 'air' then
            local cinfo = meta:get_string('infotext')
            local cname = cinfo:match("[^']+") or node.name
            table.insert(corpses, _lsitem(cname .. ' ' .. minetest.pos_to_string(pos)))
        else
            table.insert(corpses, _lsitem('<nil> ' .. minetest.pos_to_string(pos)))
        end
        count = count + 1
    end
    return count, corpses
end


--[[ -------------------- tdc namespace -------------------- ]]--
tdc = {}

function tdc.fix_corpses(td_user, map)
    local pmap = map or '*'
    local count = 0

    for mapid, cplist in pairs(lobby.corpses) do
        if pmap == '*' then
            lobby.corpse_removal(mapid)
            lobby.corpses[mapid] = {}
            count = count + 1
        else
            if mapid:find(pmap) then
                lobby.corpse_removal(mapid)
                lobby.corpses[mapid] = {}
                count = count + 1
                minetest.chat_send_player(td_user,
                    _info('Corpses in ' .. _keyw(mapid) .. ' fixed.'))
                break
            end
        end
    end
    if count == 0 then
        minetest.chat_send_player(td_user, _info('No maps found'))
    elseif pmap == '*' then
        minetest.chat_send_player(td_user,
            _info('Corpses in ' .. tostring(count) .. ' map(s) fixed.'))
    end
end

function tdc.fix_map(td_user, map)
    local pmap = map or '*'
    local count = 0

    for mapid, count in pairs(lobby.map) do
        if pmap == '*' then
            lobby.map[mapid] = 0
            lobby.update_maps(mapid)
            count = count + 1
        else
            if mapid:find(pmap) then
                lobby.map[mapid] = 0
                lobby.update_maps(mapid)
                count = count + 1
                minetest.chat_send_player(td_user,
                    _info('Map status of ' .. _keyw(mapid) .. ' fixed.'))
                break
            end
        end
    end
    if count == 0 then
        minetest.chat_send_player(td_user, _info('No maps found'))
    elseif pmap == '*' then
        minetest.chat_send_player(td_user,
            _info('Status data for ' .. tostring(count) .. ' map(s) fixed.'))
    end
end

-- Return active players whose player name matches `expr`
function tdc.active_players_matching(expr)
    local players = {}

    for _, player in pairs(minetest.get_connected_players()) do
        local pl_name = player:get_player_name()

        if pl_name:find(expr) then
            table.insert(players, player)
        end
    end
    return players
end

-- Return active map ids matching `expr`
function tdc.active_mapids_matching(expr)
    local mapids = {}

    for mapid, count in pairs(lobby.map) do
        if mapid:find(expr) and count ~= nil and count > 0 then
            mapids[mapid] = true
        end
    end

    -- The lobby itself never gets into lobby.map, so we insert it manually,
    -- since we want to treat it just like a map.
    local mapid_lobby = 'lobby'
    if mapid_lobby:find(expr) then mapids[mapid_lobby] = true end

    return mapids
end

-- Return players currently visiting a map whose id is in the list `mapids`
function tdc.players_visiting(mapids)
    local players = {}

    for pl_name, mapid in pairs(lobby.game) do
        local mid = mapid

        if mapid:find("_solo$") then
            mid = mapid:sub(1, -6)
        elseif mapid:find("_ghost$") then
            mid = mapid:sub(1, -7)
        elseif mapid:find("_builder$") then
            mid = mapid:sub(1, -9)
        end
        if mapids[mid] then
            table.insert(players, minetest.get_player_by_name(pl_name))
        end
    end
    return players
end

-- For each player in the `players` list, create an entry in the `index` table,
-- having `player:get_player_name()` as keyword.
function tdc.index_by_name(players, index)
    if players == nil or index == nil then return end

    for _, player in ipairs(players) do
        local pl_name = player:get_player_name()
        index[pl_name] = player
    end
end

-- Return a list of currently active players where either the player name
-- or the map id they're currently visiting matches `expr`.
function tdc.list_players_matching(expr)
    local players = tdc.active_players_matching(expr)
    local mapids = tdc.active_mapids_matching(expr)
    local visitors = tdc.players_visiting(mapids)
    local matches = {}

    -- index all matching players by their name
    tdc.index_by_name(players, matches)
    tdc.index_by_name(visitors, matches)

    return matches
end

-- Return a "footprint" representation of the player privileges `privs`
function tdc.privs_footprint(privs)
    local s, t, b, c, w, p, m

    if privs.server then s = 's' else s = '-' end
    if privs.traitor_dev then t = 't' else t = '-' end
    if privs.builder then b = 'b' else b = '-' end
    if privs.creative then c = 'c' else c = '-' end
    if privs.worldeditor then w = 'w' else w = '-' end
    if privs.pro_player then p = 'p' else p = '-' end
    if privs.multihome then m = 'm' else m = '-' end

    return table.concat({s, t, b, c, w, p, m})
end


-- tdc.actions: enumerates the possible /td commands
-- tdc.actions = { cmd_1 = def_1, cmd_2 = def_2, ... }
--
-- command definitions are maps:
-- def_n = {
--      info = (string, short description for "/td help")
--      help = (0-arg function, returns a longer description for "/td help <action>")
--      exec = (2-arg function, executes the action for player named <arg1> with the rest
--              of the cmdline string passed in as <arg2>)
-- }
tdc.actions = {
    -- CMD: /td help
    help = {
        info = 'Show general help and list available commands',
        help = function()
            local msgtab = {
                'Traitor debugging utility.  Type "' .. _keyw('/td <cmd>') .. '" to execute a command.',
                'Available commands:'
            }
            for cmd, action in pairs(tdc.actions) do
                table.insert(msgtab, string.format(TAB .. '%-10s\t%s', cmd, action.info))
            end
            table.insert(msgtab, '\nCommands can be abbreviated (e.g., "/td h" shows this help).')
            table.insert(msgtab, 'Type "' .. _keyw('/td help <cmd>') .. '" to get help for a specific command.')
            return table.concat(msgtab, '\n')
        end,
        exec = function(td_user, params)
            if params then
                local par1 = params:match('[%w_]+')
                if par1 and tdc.actions[par1] then
                    minetest.chat_send_player(td_user, tdc.actions[par1].help())
                elseif par1 then
                    -- TODO: find par1 as cmd prefix in tdc.actions
                    local msg = _info('Unknown command "' .. par1 .. '"\n') .. list_actions()
                    minetest.chat_send_player(td_user, msg)
                else
                    minetest.chat_send_player(td_user, tdc.actions['help'].help())
                end
            else
                minetest.chat_send_player(td_user, tdc.actions['help'].help())
            end
        end,
    },
    -- CMD: /td corpses
    corpses = {
        info = 'Show info on corpses',
        help = function()
            local msgtab = {
                _info('Usage: /td corpses [<map>]'),
                'Show corpses in map <map>.  If <map> is omitted, list all corpses.',
                'Params:',
                TAB .. '<map>   map id to search, or "*" to list all corpses'
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(td_user, params)
            local map = params:match('[%w_]+') or '*'
            local hdr, msg, ccount, clist
            if params == '' then
                -- list all corpses instead
                hdr = _info('List of corpses:\n')
                for mid, ctab in pairs(lobby.corpses) do
                    local corpses
                    if ctab then ccount = list_corpses(ctab) end
                    if ccount > 0 then
                        msg = (msg or '') .. _lsitem(mid .. ': ' .. tostring(ccount)) .. '\n'
                    end
                end
            else
                for mid, ctab in pairs(lobby.corpses) do
                    if ctab and (mid:find(map) or map == '*') then
                        hdr = _info('Corpses in ') .. _keyw(mid) .. ':\n'
                        ccount, clist = list_corpses(ctab)
                        if ccount > 0 then
                            msg = (msg or '') .. hdr .. table.concat(clist, '\n') .. '\n'
                        end
                        hdr = ''
                        if map ~= '*' then break end
                    end
                end
            end
            if msg then
                msg = hdr .. msg
            else
                msg = _info('No corpses yet.')
            end
            minetest.chat_send_player(td_user, msg)
            minetest.log('action', minetest.strip_colors(msg))
        end,
    },
    -- CMD: /td maps
    maps = {
        info = 'Show maps currently visited by players',
        help = function()
            local msgtab = {
                _info('Usage: /td maps'),
                'Show map names of all currently active game sessions.'
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(td_user, params)
            local msgtab = {_info('Active maps:')}
            local plr_count = 0
            local msg
            for mapid, plr_count in pairs(lobby.map) do
                local gh_count = 0
                local gh_map = mapid .. '_ghost'
                for _, mid in pairs(lobby.game) do
                    if mid == gh_map then
                        gh_count = gh_count + 1
                    end
                end
                if plr_count > 0 or gh_count > 0 then
                    table.insert(msgtab,
                        _lsitem(mapid .. ': ' .. plr_count .. ' player(s)')
                            .. ' / ' .. _keyw(tostring(gh_count) .. ' ghost(s)'))
                end
            end
            local clobby = 0
            for _, mid in pairs(lobby.game) do
                if mid == 'lobby' then
                    clobby = clobby + 1
                end
            end
            if clobby > 0 then
                table.insert(msgtab, _lsitem('lobby: ' .. tostring(clobby) .. ' player(s)'))
            end
            msg = table.concat(msgtab, '\n')
            minetest.chat_send_player(td_user, msg)
            minetest.log('action', minetest.strip_colors(msg))
        end,
    },
    -- CMD: /td traitors
    traitors = {
        info = 'Show the traitors in each active map',
        help = function()
            local msgtab = {
                _info('Usage: /td traitors'),
                'Show the names of all traitors in currently active game sessions'
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(td_user, params)
            local msg = ''
            for mapid, traitor in pairs(lobby.traitors) do
                -- table value could be nil if entry is to be GC'd
                if traitor then
                    msg = msg .. _lsitem(mapid .. ': ' .. traitor) .. '\n'
                end
            end
            if msg == '' then
                msg = _info('No active traitors!')
            else
                msg = _info('Active traitors:\n') .. msg
            end
            minetest.chat_send_player(td_user, msg)
            minetest.log('action', minetest.strip_colors(msg))
        end,
    },
    -- CMD: /td players <id>
    players = {
        info = 'Show player attributes',
        help = function()
            local msgtab = {
                _info('Usage: /td players <expr>'),
                'Show metadata of one or more players.',
                'Params:',
                TAB .. '<expr>  Part of a player name or map ID used as search expression.',
                TAB .. '        Any matching player name will be included in the result list.',
                TAB .. '        If <expr> is part of a map ID, lists all players currently',
                TAB .. '        visiting that map.',
                '\nNote: <expr> is restricted to alphanumeric chars and "_", for the sake of security.',
                '\nThe metadata of matching players is listed in a table with the following columns:',
                TAB .. 'Player  displays the player\'s login name',
                TAB .. 'Map     shows the map id the player is currently visiting',
                TAB .. 'Privs   shows the state of some more widely used traitor privileges, which is',
                TAB .. '        a short string where each letter symbolizes a certain privilege',
                TAB .. '        (' ..
                    _stdout('s') .. 'erver, ' ..
                    _stdout('t') .. 'raitor_dev, ' ..
                    _stdout('b') .. 'uilder, ' ..
                    _stdout('c') .. 'reative, ' ..
                    _stdout('w') .. 'orldeditor, ' ..
                    _stdout('p') .. 'ro_player, ' ..
                    _stdout('m') .. 'ultihome)',
                TAB .. '        If a player does not have a certain privilege, the privilege\'s letter',
                TAB .. '        is replaced by a \'-\' instead.',
                TAB .. 'Mode    displays the player mode',
                TAB .. 'Tone    shows the player\'s current tone color',
                TAB .. 'Spawn   prints the player\'s current spawn position (if any)',
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(td_user, params)
            if not params or not params:find("[%w_]+") then
                minetest.chat_send_player(td_user, 'Missing argument, type "/td help players" for help.')
            else
                local p1, p2, expr = params:find('([%w_]+)')
                local matches = tdc.list_players_matching(expr)
                local count = 0
                local mtab = {
                    _info('Player               Map                  Privs   Mode    Tone  Spawn'),
                    '--------------------------------------------------------------------------------------',
                }
                -- sort list by player name
                local sorted = {}
                for name in pairs(matches) do table.insert(sorted, name) end
                table.sort(sorted)

                for _, name in pairs(sorted) do
                    count = count + 1
                    local player = matches[name]
                    local attr = player:get_meta()
                    local pl_name = _keyw(name)
                    local padding = 20 - name:len()
                    local pl_padding = string.rep(' ', padding)
                    local pl_map = lobby.game[name] or '<nil>'
                    local pl_privs = tdc.privs_footprint(minetest.get_player_privs(name))
                    local pl_mode = attr:get_string('mode') or '<nil>'
                    local pl_tone = attr:get_int('tone')
                    local pl_spawn = attr:get_string('spawn_pos') or '<nil>'

                    table.insert(mtab, string.format('%s%s %-20s %-7s %-7s %5d %-20s',
                        pl_name, pl_padding, pl_map, pl_privs, pl_mode, pl_tone, pl_spawn)
                    )
                end
                if count == 0 then
                    minetest.chat_send_player(td_user, _info('No match'))
                else
                    minetest.chat_send_player(td_user, table.concat(mtab, '\n'))
                end
            end
        end,
    },
    -- CMD: /td fix (corpses|map) <map>
    --[[
        fix corpses:    call lobby.corpse_removal(<map>), then reset its poslist
        fix maps:       set lobby.map[<map>] = 0, then call lobby.update_maps(<map>)
    --]]
    fix = {
        info = 'Fix internal data',
        help = function()
            local msgtab = {
                _info('Usage: /td fix <type> <map>'),
                'Try to repair damages to game data.',
                'Params:',
                TAB .. '<type>  One of the following:',
                TAB .. '        ' .. _keyw('corpses') .. ' -- fix lobby.corpses',
                TAB .. '        ' .. _keyw('maps') .. '    -- fix lobby.map and player status',
                TAB .. '<map>   map id to fix, or "*" to fix all active maps',
                '\nNote that you can abbreviate both <type> and <map> parameters to a',
                'prefix, e.g. \"' .. _keyw('/td fix c *') .. '\" tries to fix corpses in all maps.'
            }
            return table.concat(msgtab, '\n')
        end,
        exec = function(td_user, params)
            local helpcmd = 'type "/td help fix" for help.'

            if not params then
                minetest.chat_send_player(td_user, _info('Missing arguments, ' .. helpcmd))
            else
                local p1, p2, fix, map
                print (params)
                p1, p2, fix, map = params:find('(%w+)%s+([%w_*]+)')
                if not fix or not map then
                    minetest.chat_send_player(td_user, _info('Missing arguments, ' .. helpcmd))
                else
                    if string.find('corpses', fix) then
                        tdc.fix_corpses(td_user, map)
                    elseif string.find('map', fix) then
                        tdc.fix_map(td_user, map)
                    else
                        minetest.chat_send_player(td_user, _info('Unknown fix action, ' .. helpcmd))
                    end
                end
            end
        end,
    },
    --CMD: /td mode(mode)
    mode = {
      info = 'Switch your play mode',
      help = function()
         local msgtab = {
            _info('Usage /td mode <mode>'),
            'Changes your playing mode.',
            'Params:',
            TAB .. '<mode>  One of the following:',
            TAB .. 'builder, ghost, player, solo, traitor'
         }
         return table.concat(msgtab, '\n')
      end,
      exec = function(td_user, params)
         local helpcmd = 'type "/td help mode" for help.'

         if not params then
             minetest.chat_send_player(td_user, _info('Missing arguments, ' .. helpcmd))
         else
            local modes = ' builder, ghost, player, solo, traitor'
            if string.find(modes, params) then
               local mode = string.sub(params, 2, -1)
               local player = minetest.get_player_by_name(td_user)
               local player_attributes = player:get_meta()
               player_attributes:set_string('mode', mode)
               minetest.chat_send_player(td_user, _info('Switched you to '..mode..' mode.'))
            else
               minetest.chat_send_player(td_user, _info('Missing or bad arguments, ' .. helpcmd))
            end
         end
      end,
   }

}

function tdc.exec(td_user, params)
    local cmd = nil
    if params and params ~= '' then
        local par1 = params:match('[%w_]+')
        local parN = params:sub(par1:len() + 1) or ''
        local cname
        if tdc.actions[par1] then
            cname = par1
            cmd = tdc.actions[par1]
        else
            -- try cmd prefix match
            for cmdname in pairs(tdc.actions) do
                if cmdname:find(par1) == 1 then
                    cname = cmdname
                    cmd = tdc.actions[cmdname]
                end
            end
        end
        if cname then
            minetest.log('action', td_user .. ' runs "/td ' .. cname .. parN .. '"')
            cmd.exec(td_user, parN)
        else
            minetest.chat_send_player(td_user,
                _info('Unknown command "' .. par1 .. '", type "/td help" for possible commands.'))
        end
    else
        minetest.chat_send_player(td_user, list_actions())
    end
    return true
end

minetest.register_chatcommand('td', {
   privs = {traitor_dev = true},
   params = '<cmd> [<args>]',
   description = 'Traitor debugging commands.  Type "/td help" for a list of possible commands.',
   func = tdc.exec
})
